package ru.niir.goodfil.db.handlers;

import org.apache.log4j.Logger;
import org.hibernate.StatelessSession;
import ru.niir.goodfil.audit.client.AuditService;
import ru.niir.goodfil.audit.service.AuditServiceImpl;
import ru.niir.goodfil.audit.service.DefaultConfiguration;
import ru.niir.goodfil.audit.util.Assert;
import ru.niir.goodfil.db.audit.helpers.AuditHelper;
import ru.niir.goodfil.db.audit.helpers.NullAuditHelper;
import ru.niir.goodfil.exceptions.InvalidIdException;
import ru.niir.goodfil.exceptions.NotEntityException;
import ru.niir.goodfil.exceptions.ValidationException;
import ru.niir.goodfil.utils.validation.CreateCheckResult;
import ru.niir.goodfil.utils.validation.RemoveCheckResult;
import ru.niir.goodfil.utils.validation.ValidationResult;

import java.util.List;

/**
 * Базовый класс для обработчиков событий сущностей.
 * Схема работы такова:
 * Центральной частью проекта является набор сущностей (hibernate entities), соответствующий БД goodwill-store. (БД является частью ИНТЕРНЕТ-системы)
 * Бибилиотека GoodwillCore обеспечивает целостность БД, проверяя все объекты-сущности перед выполнением запросов (INSERT, DELETE, UPDATE).
 * Для этого в пространстве имен ru.niir.goodfil.db.handler содержится набор классов, реализующих функции проверки данных для каждого из классов-сущностей.
 * (Например для проверки объекта Good есть класс GoodHandler). Все эти классы наследованы от данного и реализуют методы для проверки данных.
 * <p/>
 * Данный класс, являясь базовым для других обработчиков, содержит, помимо пустых функций проверки вспомогательные методы для работы с БД.
 * В основном, это обертки над одноименными методами оъекта Goodfil. Обработчики используеют объект Goodfil для работы с БД, т.к. реализуют только
 * логику проверки целостности данных, а сами методы для работы с БД реализует объект Goodfil.
 * <p/>
 * Объект Goodfil вызывает соответствующие методы обработчика при выполнении операций INSERT, UPDATE, DELETE.
 *
 * @see ru.niir.goodfil.db.handlers.Goodfil для более подробной информации о том, как объект Goodfil спользует обработчики.
 */
public abstract class Handler {
    private static final Logger _logger = Logger.getLogger(Handler.class);

    /**
     * Класс, с которым связан данный обработчик
     */
    protected final Class klass;

    /**
     * Объект Goodfil, используемый для доступа к БД
     */
    protected Goodfil gf;

    private AuditHelper auditHelper = new NullAuditHelper();

    private AuditService auditService = new AuditServiceImpl(new DefaultConfiguration());

    /**
     * Конструктор
     *
     * @param klass класс, с которым связан данный обработчик
     * @param gf    jбъект Goodfil, используемый для доступа к БД
     */
    public Handler(Class klass, Goodfil gf) {
        assert klass != null;
        assert gf != null;

        this.klass = klass;
        this.gf = gf;
    }

    /**
     * Возвращает объект StatelessSession для выполнения запросов с БД
     *
     * @return объект StatelessSession
     */
    protected final StatelessSession getSession() {
        return gf.getSession();
    }

    /**
     * Возвращает объект БД (одну запись таблицы) по ее идентификатору
     *
     * @param klass тип сущности
     * @param id    идентификатор
     * @return объект, если сущность с таким идентификатором существует или null, если не существует
     * @throws NotEntityException если переданный первым параметром класс не является сущностью
     */
    protected final Object get(Class klass, long id) {
        return gf.get(klass, id);
    }

    /**
     * Возвращает true, если в БД существует объект с данным идентификатором
     *
     * @param klass тип сущности
     * @param id    идентификатор
     * @return true, если в БД существует объект с данным идентификатором
     * @throws NotEntityException если переданный первым параметром класс не является сущностью
     */
    public final boolean exists(Class klass, long id) {
        return gf.exists(klass, id);
    }

    /**
     * Возвращает все объекты данного типа, содержащиеся в БД
     *
     * @param klass тип сущности
     * @return все объекты данного типа, содержащиеся в БД
     * @throws NotEntityException если переданный первым параметром класс не является сущностью
     */
    public final List get(Class klass) {
        return gf.get(klass);
    }

    /**
     * Возвращает true, если проверка на уникальность объекта с указанными атрибутами выполняется.
     * <p/>
     * Для проверки, помимо типа сущности, задается два условия:
     * 1. равенство значения поля fieldName значению fieldValue
     * 2. неравенство идентификатора объекта значению id.
     * Это позволяет применять эту функцию для проверок на уникальность значения некоторого поля в рамках таблицы.
     * Например, вызов isUnique(A, "s1", "v1", -1) проверяет: является ли значение (v1) поля s1 уникальным в таблице А.
     * Dызов isUnique(A, "s1", "v1", 10) проверяет: является ли значение (v1) поля s1 уникальным в таблице А, не считая записи с идентификатором 10.
     * Это позволяет использовать функцию как для проверки уникальности поля при создании объекта (когда его идентификатор равен -1),
     * так и при выполнении запроса UPDATE (когда идентификатор объекта имеет целое положительное значение).
     *
     * @param klass      тип сущности
     * @param fieldName  наименование поля
     * @param fieldValue значение поля
     * @param id         идентификатор объекта, игнорируемого при проверке уникальности значения поля
     * @return true, если проверка на уникальность объекта с указанными атрибутами выполняется
     */
    public final boolean isUnique(Class klass, String fieldName, Object fieldValue, long id) {
        return gf.isUnique(klass, fieldName, fieldValue, id);
    }

    /**
     * Возвращает количество записей для сущности klass, для которых выполняется два условия:
     * 1. fieldName == fieldValue
     * 2. идентификатор сущности не равен значению id
     *
     * @param klass      тип сущности
     * @param fieldName  наименование поля
     * @param fieldValue значение поля
     * @param id         идентификатор объекта, игнорируемого при проверке уникальности значения поля
     * @return количество записей для сущности klass, для которых выполняются заданные условия
     */
    public final long count(Class klass, String fieldName, Object fieldValue, long id) {
        return gf.count(klass, fieldName, fieldValue, id);
    }

    //
    //  Ниже расположены функции, реализующие логику проверок
    //

    /**
     * Проверяет состояние объект на наличие ошибок
     *
     * @param o объект
     * @return результат проверки
     * @see ru.niir.goodfil.utils.validation.ValidationResult структура, содержащая результаты проверки состояния объекта
     */
    public ValidationResult validate(Object o) {
        if (!o.getClass().equals(klass)) throw new NotEntityException(o.getClass());

        _logger.warn("Default validation procedure for class " + this.klass.getName() + ": " + String.valueOf(o));
        return ValidationResult.Ok(klass);
    }

    /**
     * Проверяет, можно ли удалить запись с указанным идентификатором, не нарушая целосность БД
     *
     * @param id идентификатор объекта
     * @return результат проверки
     * @throws InvalidIdException если запись с указанным идентификатором отсутствует в БД
     * @see ru.niir.goodfil.utils.validation.RemoveCheckResult структура, содержащая результаты проверки объекта на возможность удаления
     */
    public RemoveCheckResult removeCheck(long id) throws InvalidIdException {
        if (!exists(klass, id)) throw new InvalidIdException(klass, id);

        _logger.warn("Default removeCheck procedure for class " + this.klass.getName() + ": " + String.valueOf(id));
        return RemoveCheckResult.Ok(klass);
    }

    /**
     * Проверяет, можно ли добавить объект в БД, не нарушая ее целостность
     *
     * @param o объект
     * @return результат проверки
     * @throws ValidationException если объект содержит ошибки в своем состоянии
     * @see ru.niir.goodfil.utils.validation.CreateCheckResult структура, содержащая результаты проверки объекта на возможность его добавления (insert) в БД
     */
    public CreateCheckResult createCheck(Object o) throws ValidationException {
        if (!o.getClass().equals(klass)) throw new NotEntityException(o.getClass());

        _logger.warn("Default createCheck procedure for class " + this.klass.getName() + ": " + String.valueOf(o));
        return CreateCheckResult.Ok(klass);
    }

    /**
     * Callback, вызывается перед удалением объекта с указанным идентификатором.
     * Типичный сценарий применения: когда до удаления объекта, надо удалить связанные с ним сущности.
     * Кстати, в функции preRemove не должно ничего проверяться, проверки должны быть в методе removeCheck.
     * Функция preRemove должно использоваться для (например) удаления связанных объектов.
     *
     * @param id идентификатор объекта, который собираемся удалить
     */
    public void preRemove(long id) {
        _logger.debug("Default preRemove procedure for class " + this.klass.getName() + ": " + id);
    }

    /**
     * Callback, вызывается после удаления объекта с указанным идентификатором.
     * Типичный сценарий применения: когда после удаления объекта, надо выполнить какие-то операции.
     *
     * @param id идентификатор объекта, который только что удалили
     */
    public void postRemove(long id) {
        _logger.debug("Default postRemove procedure for class " + this.klass.getName() + ": " + id);
    }

    /**
     * Callback, вызывается перед выполнением UPDATE для данного объекта.
     * Типичный сценарий применения: когда перед обновлением объекта необходимо обносить связанные сущности.
     *
     * @param o объект - сущность БД
     */
    public void preUpdate(Object o) {
        _logger.debug("Default preUpdate procedure for class " + this.klass.getName() + ": " + o);
    }

    /**
     * Callback, вызывается после выполнения UPDATE для данного объекта
     * Типичный сценарий применения: когда после обновлениея объекта необходимо выполнить какие-то действия
     *
     * @param o объект - сущность БД
     */
    public void postUpdate(Object o) {
        _logger.debug("Default postUpdate procedure for class " + this.klass.getName() + ": " + o);
    }

    /**
     * Callback, вызывается перед выполнением INSERT для данного объекта.
     * Типичный сценарий применения: когда до вставки объекта необходимо выполнить какие-то действия
     *
     * @param o объект - сущность БД
     */
    public void preSave(Object o) {
        _logger.debug("Default preSave procedure for class " + this.klass.getName() + ": " + o);
    }

    /**
     * Callback, вызывается после выполнения INSERT для данного объекта
     * Типичный сценарий применения: когда после вставки объекта необходимо выполнить какие-то действия
     *
     * @param o объект - сущность БД
     */
    public void postSave(Object o) {
        _logger.debug("Default postSave procedure for class " + this.klass.getName() + ": " + o);
    }

    /**
     * Возвращает класс, для работы с которым предназначен данный обработчик
     *
     * @return класс, для работы с которым предназначен данный обработчик
     */
    public Class getKlass() {
        return klass;
    }

    protected AuditService getAuditService() {
        return auditService;
    }

    protected AuditHelper getAuditHelper() {
        return auditHelper;
    }

    public void setAuditHelper(AuditHelper auditHelper) {
        Assert.notNull(auditHelper);
        this.auditHelper = auditHelper;
    }

    public void setAuditService(AuditService auditService) {
        Assert.notNull(auditService);
        this.auditService = auditService;
    }

    protected void afterTransactionCommitOnSave(Object o1) {
        //  nothing
    }

    protected void afterTransactionCommitOnUpdate(Object o1, Object o2) {
        // nothing
    }

    protected void afterTransactionCommitOnRemove(Object o1) {
        // nothing
    }

    /**
     * Функция служит для принудительной подгрузки к объекту всех его зависимостей
     *
     * @param o1
     * @return
     */
    public Object fullLoadObject(Object o1) {
        Assert.notNull(o1);

        return o1;
    }
}
